﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.ServiceProcess;
using System.Text;
using System.Threading;

namespace ScrollPP.NetService
{
    public partial class ScrollPPService : ServiceBase
    {
        public const string ScrollPP_ProcessName = "ScrollPP";
        public const string ScrollPP_Filename = "ScrollPP.exe";

        private ServiceTaskScheduler _taskScheduler;
        private Dictionary<int, Mutex> _processMutex = new Dictionary<int, Mutex>();

        public ScrollPPService()
        {
            InitializeComponent();
            _taskScheduler = new ServiceTaskScheduler(EventLog);
        }

        protected override void OnStart(string[] args)
        {
            base.OnStart(args);
            QueueSessionToStart(Win32.WTSGetActiveConsoleSessionId());
        }

        protected override void OnStop()
        {
            base.OnStop();
            StopAllScrollPP();
            _taskScheduler.Stop();
        }

        protected override void OnShutdown()
        {
            base.OnShutdown();
            StopAllScrollPP();
        }

        private void QueueSessionToStart(int sessionID)
        {
            _taskScheduler.Invoke(() => StartScrollPP(sessionID));
        }

        private void QueueProcessToStop(int processID)
        {
            _taskScheduler.Invoke(() => ShutdownScrollPP(processID));
        }

        protected override void OnSessionChange(SessionChangeDescription changeDescription)
        {
            base.OnSessionChange(changeDescription);
            switch (changeDescription.Reason)
            {
                case SessionChangeReason.RemoteDisconnect:
                case SessionChangeReason.SessionLogoff:
                case SessionChangeReason.SessionLock:
                    StopScrollPP(changeDescription.SessionId);
                    break;
                case SessionChangeReason.RemoteConnect:
                case SessionChangeReason.SessionLogon:
                case SessionChangeReason.SessionUnlock:
                    QueueSessionToStart(changeDescription.SessionId);
                    break;
                case SessionChangeReason.SessionRemoteControl:
                case SessionChangeReason.ConsoleConnect:
                case SessionChangeReason.ConsoleDisconnect:
                default:
                    break;
            }
        }

        private void StartScrollPP(int sessionID)
        {
            if (sessionID == 0)
                return; //don't start on session 0

            if (IsScrollPPRunning(sessionID))
                return; //Already running for current session

            string mutexName = String.Format("0855F289-FD7C-41E5-90B5-E013EE32D624-[{0}-{1}]", sessionID, DateTime.Now.Millisecond);
            bool createdNew;
            Mutex mutex = new Mutex(true, mutexName, out createdNew);

            MutexSecurity security = new MutexSecurity(mutexName, AccessControlSections.All);
            MutexAccessRule accessRule = new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null),
                                                              MutexRights.Synchronize, //should work with synchronize only
                                                              AccessControlType.Allow);
            security.AddAccessRule(accessRule);
            mutex.SetAccessControl(security);

            if(!createdNew)
            {
                EventLog.WriteEntry(String.Format("Unknown error. Could not create a named mutex with name: \"{0}\".", mutexName));
                return;
            }

            var location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            string filename = Path.Combine(location, ScrollPP_Filename);

            string cmd = String.Format("{0} -se:Global\\{1}", filename, mutexName);
            WriteDebug(String.Format("Starting ScrollPP with commandline: \"{0}\".", cmd));

            Win32.PROCESS_INFORMATION processInformation;
            if(!Win32.StartProcessAndBypassUAC(cmd, out processInformation))
            {
                EventLog.WriteEntry("Could not launch ScrollPP. StartProcessAndBypassUAC returned false.", EventLogEntryType.Error);
                mutex.Close();
            }
            else
            {
                _processMutex.Add((int)processInformation.dwProcessId, mutex);
            }
        }

        private void StopScrollPP(int sessionID)
        {
            if(IsScrollPPRunning(sessionID))
            {
                foreach (var process in GetScrollPPInstances(sessionID))
                {
                    _taskScheduler.Invoke(() => ShutdownScrollPP(process.Id));
                }
            }
        }

        private void StopAllScrollPP()
        {
            var ids = _processMutex.Select(x => x.Key).ToArray();
            foreach (var id in ids)
            {
                QueueProcessToStop(id);
            }
        }

        [MethodImpl(MethodImplOptions.Synchronized)]
        private void ShutdownScrollPP(int pid)
        {
            try
            {
                Process process = Process.GetProcessById(pid);
                Mutex mutex;
                if (_processMutex.TryGetValue(pid, out mutex))
                {
                    WriteDebug("Releasing mutex.");
                    mutex.ReleaseMutex();

                    if (!process.WaitForExit(1500))
                    {
#if RELEASE
                        process.Kill();
#endif
                    }

                    mutex.Close();
                    _processMutex.Remove(pid);
                }
                else
                {
                    process.Kill();
                }
            }
            catch (Exception ex)
            {
                EventLog.WriteEntry("Could not stop ScrollPP: " + ex.ToString());
            }
        }

        private bool IsScrollPPRunning(int sessionID)
        {
            return GetScrollPPInstances(sessionID).Count() > 0;
        }

        private IEnumerable<Process> GetScrollPPInstances(int sessionID)
        {
            return Process.GetProcessesByName(ScrollPP_ProcessName).Where(x => x.SessionId == sessionID);
        }

        [Conditional("DEBUG")]
        private void WriteDebug(string message, EventLogEntryType type = EventLogEntryType.Information)
        {
            EventLog.WriteEntry(message, type);
        }
    }
}
